/*
 * $Id$
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package cn.calm.osgi.conter;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import javax.servlet.ServletContext;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.framework.FrameworkFactory;
import org.apache.felix.framework.util.FelixConstants;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.launch.Framework;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cn.calm.osgi.conter.util.AutoProcessor;
import cn.calm.osgi.conter.util.FelixPropertiesUtil;
import cn.calm.osgi.conter.util.Resource;


/**
 * Apache felix implementation of an OsgiHost See
 * http://felix.apache.org/site/apache
 * -felix-framework-launching-and-embedding.html <br/>
 * Servlet config params:
 * <p>
 * struts.osgi.clearBundleCache: Defaults to "true" delete installed bundles
 * when the comntainer starts
 * </p>
 * <p>
 * struts.osgi.logLevel: Defaults to "1". Felix log level. 1 = error, 2 =
 * warning, 3 = information, and 4 = debug
 * </p>
 * <p>
 * struts.osgi.runLevel: Defaults to "3". Run level to start the container.
 * </p>
 */
public class FelixOsgiHost implements OsgiHost {
	private static final Logger LOG = LoggerFactory
			.getLogger(FelixOsgiHost.class);
	Resource resource;
	Framework f;
	private ServletContext servletContext;

	protected void startFelix() {
		
		// load properties from felix embedded file
		Properties configProps = getProperties("default.properties");

		// Copy framework properties from the system properties.
		FelixPropertiesUtil.copySystemProperties(configProps);
		replaceSystemPackages(configProps);

		// struts, xwork and felix exported packages
		Properties strutsConfigProps = getProperties("osgi.properties");
		addExportedPackages(strutsConfigProps, configProps);

		// find bundles and adde em to autostart property
		addAutoStartBundles(configProps);

		// Bundle cache
		String storageDir = System.getProperty("java.io.tmpdir")
				+ ".felix-cache";
		configProps.setProperty(Constants.FRAMEWORK_STORAGE, storageDir);
		if (LOG.isDebugEnabled())
			LOG.debug("Storing bundles at [#0]", storageDir);

		String cleanBundleCache = getServletContextParam(
				"struts.osgi.clearBundleCache", "true");
		if ("true".equalsIgnoreCase(cleanBundleCache)) {
			if (LOG.isDebugEnabled())
				LOG.debug("Clearing bundle cache");
			configProps.put(FelixConstants.FRAMEWORK_STORAGE_CLEAN,
					FelixConstants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
		}

		configProps.put(FelixConstants.SERVICE_URLHANDLERS_PROP, "false");
		configProps.put(FelixConstants.LOG_LEVEL_PROP,
				getServletContextParam("struts.osgi.logLevel", "3"));
		configProps.put(FelixConstants.BUNDLE_CLASSPATH, ".");

		configProps.put("org.osgi.framework.startlevel.beginning",
				getServletContextParam("struts.osgi.runLevel", "3"));
		//
		try {
			Map<String, String> map =new TreeMap<String, String>(
					new Comparator<String>() {
						public int compare(String obj1, String obj2) {
							// 降序排序
							return obj1.compareTo(obj2);
						}
					});
			for (Object o : configProps.keySet()) {
				map.put(String.valueOf(o), configProps.getProperty(String.valueOf(o)));
			}
			FrameworkFactory ff = new FrameworkFactory();

			f = ff.newFramework(map);
			// f.start();
			f.init();

			AutoProcessor.process(map, f.getBundleContext());
			f.start();

		} catch (Exception ex) {
			throw new RuntimeException("Couldn't start Apache Felix", ex);
		}

		// add the bundle context to the ServletContext
		servletContext.setAttribute(OSGI_BUNDLE_CONTEXT, f.getBundleContext());
	}

	/**
	 * Gets a param from the ServletContext, returning the default value if the
	 * param is not set
	 * 
	 * @param paramName
	 *            the name of the param to get from the ServletContext
	 * @param defaultValue
	 *            value to return if the param is not set
	 * @return
	 */
	private String getServletContextParam(String paramName, String defaultValue) {
		return StringUtils.defaultString(
				this.servletContext.getInitParameter(paramName), defaultValue);
	}

	protected void addAutoStartBundles(Properties configProps) {
		List<String> bundleJarsLevel1 = new ArrayList<String>();
		configProps.put(AutoProcessor.AUTO_START_PROP + ".1",
				StringUtils.join(bundleJarsLevel1, " "));

		// get a list of directories under /bundles with numeric names (the
		// runlevel)
		Map<String, String> runLevels = getRunLevelDirs("bundles");
		if (runLevels.isEmpty()) {
			// there are no run level dirs, search for bundles in that dir
			List<String> bundles = getBundlesInDir("bundles");
			if (!bundles.isEmpty())
				configProps.put(AutoProcessor.AUTO_START_PROP + ".2",
						StringUtils.join(bundles, " "));
		} else {
			for (String runLevel : runLevels.keySet()) {
				if ("1".endsWith(runLevel))
					throw new RuntimeException(
							"Run level dirs must be greater than 1. Run level 1 is reserved for the Felix bundles");
				List<String> bundles = getBundlesInDir(runLevels.get(runLevel));
				configProps.put(AutoProcessor.AUTO_START_PROP + "." + runLevel,
						StringUtils.join(bundles, " "));
			}
		}
	}

	/**
	 * Return a list of directories under a directory whose name is a number
	 */
	protected Map<String, String> getRunLevelDirs(String dir) {
		Map<String, String> dirs = new HashMap<String, String>();
		try {
			
			URL url = resource.find("bundles");
			if (url != null) {
				if ("file".equals(url.getProtocol())) {
					File bundlesDir = new File(url.toURI());
					String[] runLevelDirs = bundlesDir
							.list(new FilenameFilter() {
								public boolean accept(File file, String name) {
									try {
										return file.isDirectory()
												&& Integer.valueOf(name) > 0;
									} catch (NumberFormatException ex) {
										// the name is not a number
										return false;
									}
								}
							});

					if (runLevelDirs != null && runLevelDirs.length > 0) {
						// add all the dirs to the list
						for (String runLevel : runLevelDirs)
							dirs.put(runLevel, StringUtils.removeEnd(dir, "/")
									+ "/" + runLevel);

					} else if (LOG.isDebugEnabled()) {
						LOG.debug(
								"No run level directories found under the [#0] directory",
								dir);
					}
				} else if (LOG.isWarnEnabled())
					LOG.warn("Unable to read [#0] directory", dir);
			} else if (LOG.isWarnEnabled())
				LOG.warn("The [#0] directory was not found", dir);
		} catch (Exception e) {
			if (LOG.isWarnEnabled())
				LOG.warn("Unable load bundles from the [#0] directory", e, dir);
		}
		return dirs;
	}

	protected List<String> getBundlesInDir(String dir) {
		List<String> bundleJars = new ArrayList<String>();
		try {
			URL url = resource.find(dir);
			if (url != null) {
				if ("file".equals(url.getProtocol())) {
					File bundlesDir = new File(url.toURI());
					File[] bundles = bundlesDir.listFiles(new FilenameFilter() {
						public boolean accept(File file, String name) {
							return StringUtils.endsWith(name, ".jar");
						}
					});

					if (bundles != null && bundles.length > 0) {
						// add all the bundles to the list
						for (File bundle : bundles) {
							String externalForm = bundle.toURI().toURL()
									.toExternalForm();
							if (LOG.isDebugEnabled()) {
								LOG.debug("Adding bundle [#0]", externalForm);
							}
							bundleJars.add(externalForm);
						}

					} else if (LOG.isDebugEnabled()) {
						LOG.debug("No bundles found under the [#0] directory",
								dir);
					}
				} else if (LOG.isWarnEnabled())
					LOG.warn("Unable to read [#0] directory", dir);
			} else if (LOG.isWarnEnabled())
				LOG.warn("The [#0] directory was not found", dir);
		} catch (Exception e) {
			if (LOG.isWarnEnabled())
				LOG.warn("Unable load bundles from the [#0] directory", e, dir);
		}
		return bundleJars;
	}
//
//
	protected void replaceSystemPackages(Properties properties) {
		// Felix has a way to load the config file and substitution expressions
		// but the method does not have a way to specify the file (other than in
		// an env variable)

		// ${jre-${java.specification.version}}
		String systemPackages = (String) properties
				.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
		String jreVersion = "jre-"
				+ System.getProperty("java.version").substring(0, 3);
		systemPackages = systemPackages.replace(
				"${jre-${java.specification.version}}",
				(String) properties.get(jreVersion));
		properties.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
	}
//
	/*
	 * Find subpackages of the packages defined in the property file and export
	 * them
	 */
	protected void addExportedPackages(Properties strutsConfigProps,
			Properties configProps) {
		String[] rootPackages = StringUtils.split(
				 strutsConfigProps.getProperty("scanning.package.includes"),
				",");
		List<String> exportedPackages = new ArrayList<String>();
		// build a list of subpackages
		StringBuilder strB=new StringBuilder(); 
//		org.osgi.framework;version="[1.4,2)"
		for (String rootPackage : rootPackages) {
			String sp[]=StringUtils.split(rootPackage,";");
			strB.delete(0, strB.length());
			strB.append(sp[0]);
			strB.append(";version=\"");
			try{
				strB.append(sp[1]);
			}catch (Exception e) {
				strB.append("0.0.0");
			}
			strB.append("\"");
			exportedPackages.add(strB.toString());
		}
//
//		// make a string with the exported packages and add it to the system
//		// properties
		if (!exportedPackages.isEmpty()) {
			String systemPackages = (String) configProps
					.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
			systemPackages = StringUtils.removeEnd(systemPackages, ",") + ","
					+ StringUtils.join(exportedPackages, ",");
			configProps.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
		}
		System.out.println(exportedPackages);
	}
//
//	/**
//	 * Gets the version used to export the packages. it tries to get it from
//	 * MANIFEST.MF, or the file name
//	 */
//	protected String getVersion(URL url) {
//		if ("jar".equals(url.getProtocol())) {
//			try {
////				FileManager fileManager = ServletActionContext.getContext()
////						.getInstance(FileManagerFactory.class).getFileManager();
//				String file=url.toURI().toString();
//				file=file.substring(10, file.lastIndexOf("!"));
//				JarFile jarFile = new JarFile(file);
//				Manifest manifest = jarFile.getManifest();
//				if (manifest != null) {
//					String version = manifest.getMainAttributes().getValue(
//							"Bundle-Version");
//					if (StringUtils.isNotBlank(version)) {
//						return getVersionFromString(version);
//					}
//				} else {
//					// try to get the version from the file name
//					return getVersionFromString(jarFile.getName());
//				}
//			} catch (Exception e) {
//				if (LOG.isErrorEnabled())
//					LOG.error(
//							"Unable to extract version from [#0], defaulting to '1.0.0'",
//							url.toExternalForm());
//
//			}
//		}
//
//		return "1.0.0";
//	}

//	/**
//	 * Extracts numbers followed by "." or "-" from the string and joins them
//	 * with "."
//	 */
//	protected static String getVersionFromString(String str) {
//		Matcher matcher = versionPattern.matcher(str);
//		List<String> parts = new ArrayList<String>();
//		while (matcher.find()) {
//			parts.add(matcher.group(1));
//		}
//
//		// default
//		if (parts.size() == 0)
//			return "1.0.0";
//
//		while (parts.size() < 3)
//			parts.add("0");
//
//		return StringUtils.join(parts, ".");
//	}

	protected Properties getProperties(String fileName) {
		
		try {
			return resource.findProperties(fileName);
		} catch (IOException e) {
			if (LOG.isErrorEnabled())
				LOG.error("Unable to read property file [#]", fileName);
			return new Properties();
		}
	}

	/**
	 * This bundle map will not change, but the status of the bundles can change
	 * over time. Use getActiveBundles() for active bundles
	 */
	public Map<String, Bundle> getBundles() {
		Map<String, Bundle> bundles = new HashMap<String, Bundle>();
		for (Bundle bundle : f.getBundleContext().getBundles()) {
			bundles.put(bundle.getSymbolicName(), bundle);
		}

		return Collections.unmodifiableMap(bundles);
	}

	public Map<String, Bundle> getActiveBundles() {
		Map<String, Bundle> bundles = new HashMap<String, Bundle>();
		for (Bundle bundle : f.getBundleContext().getBundles()) {
			if (bundle.getState() == Bundle.ACTIVE)
				bundles.put(bundle.getSymbolicName(), bundle);
		}

		return Collections.unmodifiableMap(bundles);
	}

	public BundleContext getBundleContext() {
		return f.getBundleContext();
	}

	public void destroy() throws Exception {
		f.stop();
		if (LOG.isTraceEnabled())
			LOG.trace("Apache Felix has stopped");
	}

	public void init(ServletContext servletContext) {
		this.servletContext = servletContext;
		resource=Resource.getResource(servletContext);
		startFelix();
	}
}
